package org.tron.plugins.utils;

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystemException;
import java.nio.file.FileVisitOption;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.List;
import java.util.Properties;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class FileUtils {

  public static boolean isLevelDBEngine(Path path) {
    String dir = path.toString();
    String enginePath = dir + File.separator + "engine.properties";
    if (!new File(enginePath).exists()
        && !writeProperty(enginePath, DBUtils.KEY_ENGINE, DBUtils.LEVELDB)) {
      return false;
    }
    String engine = readProperty(enginePath, DBUtils.KEY_ENGINE);
    return DBUtils.LEVELDB.equals(engine);
  }

  public static String readProperty(String file, String key) {
    try (FileInputStream fileInputStream = new FileInputStream(file);
         InputStream inputStream = new BufferedInputStream(fileInputStream)) {
      Properties prop = new Properties();
      prop.load(inputStream);
      return new String(prop.getProperty(key, "").getBytes(StandardCharsets.ISO_8859_1),
          StandardCharsets.UTF_8);
    } catch (Exception e) {
      logger.error("readProperty", e);
      return "";
    }
  }

  public static boolean writeProperty(String file, String key, String value) {
    try (OutputStream o = new FileOutputStream(file);
         FileInputStream f = new FileInputStream(file);
         BufferedWriter w = new BufferedWriter(new OutputStreamWriter(o, StandardCharsets.UTF_8));
         BufferedReader r = new BufferedReader(new InputStreamReader(f, StandardCharsets.UTF_8))
    ) {
      Properties properties = new Properties();
      properties.load(r);
      properties.setProperty(key, value);
      properties.store(w, "Generated by the application.  PLEASE DO NOT EDIT! ");
    } catch (Exception e) {
      logger.warn("writeProperty", e);
      return false;
    }
    return true;
  }


  /**
   * delete directory.
   */
  public static boolean deleteDir(File dir) {
    if (dir.isDirectory()) {
      String[] children = dir.list();
      for (int i = 0; i < children.length; i++) {
        boolean success = deleteDir(new File(dir, children[i]));
        if (!success) {
          return false;
        }
      }
    }
    return dir.delete();
  }

  public static boolean createFileIfNotExists(String filepath) {
    File file = new File(filepath);
    if (!file.exists()) {
      try {
        file.createNewFile();
      } catch (Exception e) {
        return false;
      }
    }
    return true;
  }

  public static boolean createDirIfNotExists(String dirPath) {
    File dir = new File(dirPath);
    if (!dir.exists()) {
      return dir.mkdirs();
    }
    return true;
  }

  public static boolean isExists(String path) {
    File file = new File(path);
    return file.exists();
  }

  public static boolean isSymbolicLink(File file) throws IOException {
    if (file == null) {
      throw new NullPointerException("File must not be null");
    }

    File canon;
    if (file.getParent() == null) {
      canon = file;
    } else {
      File canonDir = file.getParentFile().getCanonicalFile();
      canon = new File(canonDir, file.getName());
    }
    return !canon.getCanonicalFile().equals(canon.getAbsoluteFile());
  }

  /**
   * Copy src to dest, if dest is a directory and already exists, throw Exception.
   *
   * <p>Note: This method is not rigorous, because all the dirs that its FileName
   * is contained in List(subDirs) will be filtered, this may result in unpredictable result.
   * just used in LiteFullNodeTool.
   *
   * @param src     Path or File
   * @param dest    Path or File
   * @param subDirs only the subDirs in {@code src} will be copied
   * @throws IOException IOException
   */
  public static void copyDatabases(Path src, Path dest, List<String> subDirs)
      throws IOException {
    // create subdirs, as using parallel() to run, so should create dirs first.
    subDirs.forEach(dir -> {
      if (isExists(Paths.get(src.toString(), dir).toString())) {
        try {
          Files.walk(Paths.get(src.toString(), dir), FileVisitOption.FOLLOW_LINKS)
              .forEach(source -> copy(source, dest.resolve(src.relativize(source))));
        } catch (IOException e) {
          logger.error("copy database failed, src: {}, dest: {}, error: {}",
              Paths.get(src.toString(), dir), Paths.get(dest.toString(), dir), e.getMessage());
          throw new RuntimeException(e);
        }
      }
    });
  }

  public static void copyDir(Path src, Path dest, String dir) {
    if (isExists(Paths.get(src.toString(), dir).toString())) {
      try {
        if (createDirIfNotExists(Paths.get(dest.toString(), dir).toString())) {
          Files.walk(Paths.get(src.toString(), dir), FileVisitOption.FOLLOW_LINKS)
              .forEach(source -> copy(source, dest.resolve(src.relativize(source))));
        } else {
          throw new IOException(String.format("dest %s create fail ",
              Paths.get(dest.toString(), dir)));
        }
      } catch (IOException e) {
        logger.error("copy dir failed, src: {}, dest: {}, error: {}",
            Paths.get(src.toString(), dir), Paths.get(dest.toString(), dir), e.getMessage());
        throw new RuntimeException(e);
      }
    }
  }

  public static void copy(Path source, Path dest) {
    try {
      // create hard link when file is .sst
      if (source.toString().endsWith(".sst")) {
        try {
          java.nio.file.Files.createLink(dest, source);
        } catch (FileSystemException e) {
          java.nio.file.Files.copy(source, dest, StandardCopyOption.REPLACE_EXISTING);
        }
      } else {
        java.nio.file.Files.copy(source, dest, StandardCopyOption.REPLACE_EXISTING);
      }
    } catch (Exception e) {
      throw new RuntimeException(e.getMessage(), e);
    }
  }
}
